using UnityEngine; using UnityEngine.UI; using System.Collections; using System; namespace EnhancedUI.EnhancedScroller { /// /// This delegate handles the visibility changes of cell views /// /// The cell view that changed visibility public delegate void CellViewVisibilityChangedDelegate(EnhancedScrollerCellView cellView); /// /// This delegate handles the scrolling callback of the ScrollRect. /// /// The scroller that called the delegate /// The scroll value of the scroll rect /// The scroll position in pixels from the start of the scroller public delegate void ScrollerScrolledDelegate(EnhancedScroller scroller, Vector2 val, float scrollPosition); /// /// This delegate handles the snapping of the scroller. /// /// The scroller that called the delegate /// The index of the cell view snapped on (this may be different than the data index in case of looping) /// The index of the data the view snapped on public delegate void ScrollerSnappedDelegate(EnhancedScroller scroller, int cellIndex, int dataIndex); /// /// This delegate handles the change in state of the scroller (scrolling or not scrolling) /// /// The scroller that changed state /// Whether or not the scroller is scrolling public delegate void ScrollerScrollingChangedDelegate(EnhancedScroller scroller, bool scrolling); /// /// This delegate handles the change in state of the scroller (jumping or not jumping) /// /// The scroller that changed state /// Whether or not the scroller is tweening public delegate void ScrollerTweeningChangedDelegate(EnhancedScroller scroller, bool tweening); /// /// The EnhancedScroller allows you to easily set up a dynamic scroller that will recycle views for you. This means /// that using only a handful of views, you can display thousands of rows. This will save memory and processing /// power in your application. /// [RequireComponent(typeof(ScrollRect))] public class EnhancedScroller : MonoBehaviour { #region Public /// /// The direction this scroller is handling /// public enum ScrollDirectionEnum { Vertical, Horizontal } /// /// Which side of a cell to reference. /// For vertical scrollers, before means above, after means below. /// For horizontal scrollers, before means to left of, after means to the right of. /// public enum CellViewPositionEnum { Before, After } /// /// This will set how the scroll bar should be shown based on the data. If no scrollbar /// is attached, then this is ignored. OnlyIfNeeded will hide the scrollbar based on whether /// the scroller is looping or there aren't enough items to scroll. /// public enum ScrollbarVisibilityEnum { OnlyIfNeeded, Always, Never } /// /// The direction the scroller is handling /// public ScrollDirectionEnum scrollDirection; /// /// The number of pixels between cell views, starting after the first cell view /// public float spacing; /// /// The padding inside of the scroller: top, bottom, left, right. /// public RectOffset padding; /// /// Whether the scroller should loop the cell views /// [SerializeField] private bool loop; /// /// Whether the scollbar should be shown /// [SerializeField] private ScrollbarVisibilityEnum scrollbarVisibility; /// /// Whether snapping is turned on /// public bool snapping; /// /// This is the speed that will initiate the snap. When the /// scroller slows down to this speed it will snap to the location /// specified. /// public float snapVelocityThreshold; /// /// The snap offset to watch for. When the snap occurs, this /// location in the scroller will be how which cell to snap to /// is determined. /// Typically, the offset is in the range 0..1, with 0 being /// the top / left of the scroller and 1 being the bottom / right. /// In most situations the watch offset and the jump offset /// will be the same, they are just separated in case you need /// that added functionality. /// public float snapWatchOffset; /// /// The snap location to move the cell to. When the snap occurs, /// this location in the scroller will be where the snapped cell /// is moved to. /// Typically, the offset is in the range 0..1, with 0 being /// the top / left of the scroller and 1 being the bottom / right. /// In most situations the watch offset and the jump offset /// will be the same, they are just separated in case you need /// that added functionality. /// public float snapJumpToOffset; /// /// Once the cell has been snapped to the scroller location, this /// value will determine how the cell is centered on that scroller /// location. /// Typically, the offset is in the range 0..1, with 0 being /// the top / left of the cell and 1 being the bottom / right. /// public float snapCellCenterOffset; /// /// Whether to include the spacing between cells when determining the /// cell offset centering. /// public bool snapUseCellSpacing; /// /// What function to use when interpolating between the current /// scroll position and the snap location. This is also known as easing. /// If you want to go immediately to the snap location you can either /// set the snapTweenType to immediate or set the snapTweenTime to zero. /// public TweenType snapTweenType; /// /// The time it takes to interpolate between the current scroll /// position and the snap location. /// If you want to go immediately to the snap location you can either /// set the snapTweenType to immediate or set the snapTweenTime to zero. /// public float snapTweenTime; /// /// This delegate is called when a cell view is hidden or shown /// public CellViewVisibilityChangedDelegate cellViewVisibilityChanged; /// /// This delegate is called when the scroll rect scrolls /// public ScrollerScrolledDelegate scrollerScrolled; /// /// This delegate is called when the scroller has snapped to a position /// public ScrollerSnappedDelegate scrollerSnapped; /// /// This delegate is called when the scroller has started or stopped scrolling /// public ScrollerScrollingChangedDelegate scrollerScrollingChanged; /// /// This delegate is called when the scroller has started or stopped tweening /// public ScrollerTweeningChangedDelegate scrollerTweeningChanged; /// /// The Delegate is what the scroller will call when it needs to know information about /// the underlying data or views. This allows a true MVC process. /// public IEnhancedScrollerDelegate Delegate { get { return _delegate; } set { _delegate = value; _reloadData = true; } } /// /// The absolute position in pixels from the start of the scroller /// public float ScrollPosition { get { return _scrollPosition; } set { // make sure the position is in the bounds of the current set of views value = Mathf.Clamp(value, 0, GetScrollPositionForCellViewIndex(_cellViewSizeArray.Count - 1, CellViewPositionEnum.Before)); // only if the value has changed if (_scrollPosition != value) { _scrollPosition = value; if (scrollDirection == ScrollDirectionEnum.Vertical) { // set the vertical position _scrollRect.verticalNormalizedPosition = 1f - (_scrollPosition / _ScrollSize); } else { // set the horizontal position _scrollRect.horizontalNormalizedPosition = (_scrollPosition / _ScrollSize); } // flag that we need to refresh _refreshActive = true; } } } /// /// Whether the scroller should loop the resulting cell views. /// Looping creates three sets of internal size data, attempting /// to keep the scroller in the middle set. If the scroller goes /// outside of this set, it will jump back into the middle set, /// giving the illusion of an infinite set of data. /// public bool Loop { get { return loop; } set { // only if the value has changed if (loop != value) { // get the original position so that when we turn looping on // we can jump back to this position var originalScrollPosition = _scrollPosition; loop = value; // call resize to generate more internal elements if loop is on, // remove the elements if loop is off _Resize(false); if (loop) { // set the new scroll position based on the middle set of data + the original position ScrollPosition = _loopFirstScrollPosition + originalScrollPosition; } else { // set the new scroll position based on the original position and the first loop position ScrollPosition = originalScrollPosition - _loopFirstScrollPosition; } // update the scrollbars ScrollbarVisibility = scrollbarVisibility; } } } /// /// Sets how the visibility of the scrollbars should be handled /// public ScrollbarVisibilityEnum ScrollbarVisibility { get { return scrollbarVisibility; } set { scrollbarVisibility = value; // only if the scrollbar exists if (_scrollbar != null) { // make sure we actually have some cell views if (_cellViewOffsetArray != null && _cellViewOffsetArray.Count > 0) { if (_cellViewOffsetArray.Last() < ScrollRectSize || loop) { // if the size of the scrollable area is smaller than the scroller // or if we have looping on, hide the scrollbar unless the visibility // is set to Always. _scrollbar.gameObject.SetActive(scrollbarVisibility == ScrollbarVisibilityEnum.Always); } else { // if the size of the scrollable areas is larger than the scroller // or looping is off, then show the scrollbars unless visibility // is set to Never. _scrollbar.gameObject.SetActive(scrollbarVisibility != ScrollbarVisibilityEnum.Never); } } } } } /// /// This is the velocity of the scroller. /// public Vector2 Velocity { get { return _scrollRect.velocity; } set { _scrollRect.velocity = value; } } /// /// The linear velocity is the velocity on one axis. /// The scroller should only be moving one one axix. /// public float LinearVelocity { get { // return the velocity component depending on which direction this is scrolling return (scrollDirection == ScrollDirectionEnum.Vertical ? _scrollRect.velocity.y : _scrollRect.velocity.x); } set { // set the appropriate component of the velocity if (scrollDirection == ScrollDirectionEnum.Vertical) { _scrollRect.velocity = new Vector2(0, value); } else { _scrollRect.velocity = new Vector2(value, 0); } } } /// /// Whether the scroller is scrolling or not /// public bool IsScrolling { get; private set; } /// /// Whether the scroller is tweening or not /// public bool IsTweening { get; private set; } /// /// This is the first cell view index showing in the scroller's visible area /// public int StartCellViewIndex { get { return _activeCellViewsStartIndex; } } /// /// This is the last cell view index showing in the scroller's visible area /// public int EndCellViewIndex { get { return _activeCellViewsEndIndex; } } /// /// This is the first data index showing in the scroller's visible area /// public int StartDataIndex { get { return _activeCellViewsStartIndex % NumberOfCells; } } /// /// This is the last data index showing in the scroller's visible area /// public int EndDataIndex { get { return _activeCellViewsEndIndex % NumberOfCells; } } /// /// This is the number of cells in the scroller /// public int NumberOfCells { get { return (_delegate != null ? _delegate.GetNumberOfCells(this) : 0); } } /// /// This is a convenience link to the scroller's scroll rect /// public ScrollRect ScrollRect { get { return _scrollRect; } } /// /// The size of the visible portion of the scroller /// public float ScrollRectSize { get { if (scrollDirection == ScrollDirectionEnum.Vertical) return _scrollRectTransform.rect.height; else return _scrollRectTransform.rect.width; } } /// /// Create a cell view, or recycle one if it already exists /// /// The prefab to use to create the cell view /// public EnhancedScrollerCellView GetCellView(EnhancedScrollerCellView cellPrefab) { // see if there is a view to recycle var cellView = _GetRecycledCellView(cellPrefab); if (cellView == null) { // no recyleable cell found, so we create a new view // and attach it to our container var go = Instantiate(cellPrefab.gameObject); cellView = go.GetComponent(); cellView.transform.SetParent(_container); RectTransform rt = go.GetComponent(); rt.anchoredPosition3D = new Vector3(rt.anchoredPosition3D.x, rt.anchoredPosition3D.y, 0); rt.localEulerAngles = Vector3.zero; } return cellView; } /// /// This resets the internal size list and refreshes the cell views /// public void ReloadData() { _reloadData = false; _scrollPosition = 0; // recycle all the active cells so // that we are sure to get fresh views _RecycleAllCells(); // if we have a delegate handling our data, then // call the resize if (_delegate != null) _Resize(false); } /// /// This calls the RefreshCellView method on each active cell. /// If you override the RefreshCellView method in your cells /// then you can update the UI without having to reload the data. /// Note: this will not change the cell sizes, you will need /// to call ReloadData for that to work. /// public void RefreshActiveCellViews() { for (var i = 0; i < _activeCellViews.Count; i++) { _activeCellViews[i].RefreshCellView(); } } /// /// Removes all the recycled cell views. This should only be used after you /// load in a completely different set of cell views that will not use the /// recycled views. This will call garbage collection. /// public void ClearRecycled() { for (var i = 0; i < _recycledCellViews.Count; i++) { DestroyImmediate(_recycledCellViews[i].gameObject); } _recycledCellViews.Clear(); } /// /// Turn looping on or off. This is just a helper function so /// you don't have to keep track of the state of the looping /// in your own scripts. /// public void ToggleLoop() { Loop = !loop; } /// /// Jump to a position in the scroller based on a dataIndex. This overload allows you /// to specify a specific offset within a cell as well. /// /// he data index to jump to /// The offset from the start (top / left) of the scroller in the range 0..1. /// Outside this range will jump to the location before or after the scroller's viewable area /// The offset from the start (top / left) of the cell in the range 0..1 /// Whether to calculate in the spacing of the scroller in the jump /// What easing to use for the jump /// How long to interpolate to the jump point /// This delegate is fired when the jump completes public void JumpToDataIndex(int dataIndex, float scrollerOffset = 0, float cellOffset = 0, bool useSpacing = true, TweenType tweenType = TweenType.immediate, float tweenTime = 0f, Action jumpComplete = null ) { var cellOffsetPosition = 0f; if (cellOffset != 0) { // calculate the cell offset position // get the cell's size var cellSize = (_delegate != null ? _delegate.GetCellViewSize(this, dataIndex) : 0); if (useSpacing) { // if using spacing add spacing from one side cellSize += spacing; // if this is not a bounday cell, then add spacing from the other side if (dataIndex > 0 && dataIndex < (NumberOfCells - 1)) cellSize += spacing; } // calculate the position based on the size of the cell and the offset within that cell cellOffsetPosition = cellSize * cellOffset; } var newScrollPosition = 0f; // cache the offset for quicker calculation var offset = -(scrollerOffset * ScrollRectSize) + cellOffsetPosition; if (loop) { // if looping, then we need to determine the closest jump position. // we do that by checking all three sets of data locations, and returning the closest one // get the scroll positions for each data set. // Note: we are calculating the position based on the cell view index, not the data index here var set1Position = GetScrollPositionForCellViewIndex(dataIndex, CellViewPositionEnum.Before) + offset; var set2Position = GetScrollPositionForCellViewIndex(dataIndex + NumberOfCells, CellViewPositionEnum.Before) + offset; var set3Position = GetScrollPositionForCellViewIndex(dataIndex + (NumberOfCells * 2), CellViewPositionEnum.Before) + offset; // get the offsets of each scroll position from the current scroll position var set1Diff = (Mathf.Abs(_scrollPosition - set1Position)); var set2Diff = (Mathf.Abs(_scrollPosition - set2Position)); var set3Diff = (Mathf.Abs(_scrollPosition - set3Position)); // choose the smallest offset from the current position (the closest position) if (set1Diff < set2Diff) { if (set1Diff < set3Diff) { newScrollPosition = set1Position; } else { newScrollPosition = set3Position; } } else { if (set2Diff < set3Diff) { newScrollPosition = set2Position; } else { newScrollPosition = set3Position; } } } else { // not looping, so just get the scroll position from the dataIndex newScrollPosition = GetScrollPositionForDataIndex(dataIndex, CellViewPositionEnum.Before) + offset; } // clamp the scroll position to a valid location newScrollPosition = Mathf.Clamp(newScrollPosition, 0, GetScrollPositionForCellViewIndex(_cellViewSizeArray.Count - 1, CellViewPositionEnum.Before)); // if spacing is used, adjust the final position if (useSpacing) { // move back by the spacing if necessary newScrollPosition = Mathf.Clamp(newScrollPosition - spacing, 0, GetScrollPositionForCellViewIndex(_cellViewSizeArray.Count - 1, CellViewPositionEnum.Before)); } // start tweening StartCoroutine(TweenPosition(tweenType, tweenTime, ScrollPosition, newScrollPosition, jumpComplete)); } /// /// Jump to a position in the scroller based on a dataIndex. /// /// The data index to jump to /// Whether you should jump before or after the cell view [System.Obsolete("This is an obsolete method, please use the version of this function with a cell offset.")] public void JumpToDataIndex(int dataIndex, CellViewPositionEnum position = CellViewPositionEnum.Before, bool useSpacing = true) { // if looping is on, we need to jump to the middle set of data, otherwise just use the dataIndex for the cellIndex ScrollPosition = GetScrollPositionForDataIndex(dataIndex, position); // if spacing is used, adjust the final position if (useSpacing) { if (position == CellViewPositionEnum.Before) ScrollPosition = _scrollPosition - spacing; else ScrollPosition = _scrollPosition + spacing; } } /// /// Snaps the scroller on command. This is called internally when snapping is set to true and the velocity /// has dropped below the threshold. You can use this to manually snap whenever you like. /// public void Snap() { if (NumberOfCells == 0) return; // set snap jumping to true so other events won't process while tweening _snapJumping = true; // stop the scroller LinearVelocity = 0; // cache the current inertia state and turn off inertia _snapInertia = _scrollRect.inertia; _scrollRect.inertia = false; // calculate the snap position var snapPosition = ScrollPosition + (ScrollRectSize * Mathf.Clamp01(snapWatchOffset)); // get the cell view index of cell at the watch location _snapCellViewIndex = GetCellViewIndexAtPosition(snapPosition); // get the data index of the cell at the watch location _snapDataIndex = _snapCellViewIndex % NumberOfCells; // jump the snapped cell to the jump offset location and center it on the cell offset JumpToDataIndex(_snapDataIndex, snapJumpToOffset, snapCellCenterOffset, snapUseCellSpacing, snapTweenType, snapTweenTime, SnapJumpComplete); } /// /// Gets the scroll position in pixels from the start of the scroller based on the cellViewIndex /// /// The cell index to look for. This is used instead of dataIndex in case of looping /// Do we want the start or end of the cell view's position /// public float GetScrollPositionForCellViewIndex(int cellViewIndex, CellViewPositionEnum insertPosition) { if (NumberOfCells == 0) return 0; if (cellViewIndex == 0 && insertPosition == CellViewPositionEnum.Before) { return 0; } else { if (cellViewIndex < _cellViewOffsetArray.Count) { // the index is in the range of cell view offsets if (insertPosition == CellViewPositionEnum.Before) { // return the previous cell view's offset + the spacing between cell views return _cellViewOffsetArray[cellViewIndex - 1] + spacing + (scrollDirection == ScrollDirectionEnum.Vertical ? padding.top : padding.left); } else { // return the offset of the cell view (offset is after the cell) return _cellViewOffsetArray[cellViewIndex] + (scrollDirection == ScrollDirectionEnum.Vertical ? padding.top : padding.left); } } else { // get the start position of the last cell (the offset of the second to last cell) return _cellViewOffsetArray[_cellViewOffsetArray.Count - 2]; } } } /// /// Gets the scroll position in pixels from the start of the scroller based on the dataIndex /// /// The data index to look for /// Do we want the start or end of the cell view's position /// public float GetScrollPositionForDataIndex(int dataIndex, CellViewPositionEnum insertPosition) { return GetScrollPositionForCellViewIndex(loop ? _delegate.GetNumberOfCells(this) + dataIndex : dataIndex, insertPosition); } /// /// Gets the index of a cell view at a given position /// /// The pixel offset from the start of the scroller /// public int GetCellViewIndexAtPosition(float position) { // call the overrloaded method on the entire range of the list return _GetCellIndexAtPosition(position, 0, _cellViewOffsetArray.Count - 1); } #endregion #region Private /// /// Cached reference to the scrollRect /// private ScrollRect _scrollRect; /// /// Cached reference to the scrollRect's transform /// private RectTransform _scrollRectTransform; /// /// Cached reference to the scrollbar if it exists /// private Scrollbar _scrollbar; /// /// Cached reference to the active cell view container /// private RectTransform _container; /// /// Cached reference to the layout group that handles view positioning /// private HorizontalOrVerticalLayoutGroup _layoutGroup; /// /// Reference to the delegate that will tell this scroller information /// about the underlying data /// private IEnhancedScrollerDelegate _delegate; /// /// Flag to tell the scroller to reload the data /// private bool _reloadData; /// /// Flag to tell the scroller to refresh the active list of cell views /// private bool _refreshActive; /// /// List of views that have been recycled /// private SmallList _recycledCellViews = new SmallList(); /// /// Cached reference to the element used to offset the first visible cell view /// private LayoutElement _firstPadder; /// /// Cached reference to the element used to keep the cell views at the correct size /// private LayoutElement _lastPadder; /// /// Cached reference to the container that holds the recycled cell views /// private RectTransform _recycledCellViewContainer; /// /// Internal list of cell view sizes. This is created when the data is reloaded /// to speed up processing. /// private SmallList _cellViewSizeArray = new SmallList(); /// /// Internal list of cell view offsets. Each cell view offset is an accumulation /// of the offsets previous to it. /// This is created when the data is reloaded to speed up processing. /// private SmallList _cellViewOffsetArray = new SmallList(); /// /// The scrollers position /// private float _scrollPosition; /// /// The list of cell views that are currently being displayed /// private SmallList _activeCellViews = new SmallList(); /// /// The index of the first cell view that is being displayed /// private int _activeCellViewsStartIndex; /// /// The index of the last cell view that is being displayed /// private int _activeCellViewsEndIndex; /// /// The index of the first element of the middle section of cell view sizes. /// Used only when looping /// private int _loopFirstCellIndex; /// /// The index of the last element of the middle seciton of cell view sizes. /// used only when looping /// private int _loopLastCellIndex; /// /// The scroll position of the first element of the middle seciotn of cell views. /// Used only when looping /// private float _loopFirstScrollPosition; /// /// The scroll position of the last element of the middle section of cell views. /// Used only when looping /// private float _loopLastScrollPosition; /// /// The position that triggers the scroller to jump to the end of the middle section /// of cell views. This keeps the scroller in the middle section as much as possible. /// private float _loopFirstJumpTrigger; /// /// The position that triggers the scroller to jump to the start of the middle section /// of cell views. This keeps the scroller in the middle section as much as possible. /// private float _loopLastJumpTrigger; /// /// The cached value of the last scroll rect size. This is checked every frame to see /// if the scroll rect has resized. If so, it will refresh. /// private float _lastScrollRectSize; /// /// The cached value of the last loop setting. This is checked every frame to see /// if looping was toggled. If so, it will refresh. /// private bool _lastLoop; /// /// The cell view index we are snapping to /// private int _snapCellViewIndex; /// /// The data index we are snapping to /// private int _snapDataIndex; /// /// Whether we are currently jumping due to a snap /// private bool _snapJumping; /// /// What the previous inertia setting was before the snap jump. /// We cache it here because we need to turn off inertia while /// manually tweeing. /// private bool _snapInertia; /// /// The cached value of the last scrollbar visibility setting. This is checked every /// frame to see if the scrollbar visibility needs to be changed. /// private ScrollbarVisibilityEnum _lastScrollbarVisibility; /// /// Where in the list we are /// private enum ListPositionEnum { First, Last } /// /// The size of the active cell view container minus the visibile portion /// of the scroller /// private float _ScrollSize { get { if (scrollDirection == ScrollDirectionEnum.Vertical) return _container.rect.height - _scrollRectTransform.rect.height; else return _container.rect.width - _scrollRectTransform.rect.width; } } /// /// This function will create an internal list of sizes and offsets to be used in all calculations. /// It also sets up the loop triggers and positions and initializes the cell views. /// /// If true, then the scroller will try to go back to the position it was at before the resize private void _Resize(bool keepPosition) { // cache the original position var originalScrollPosition = _scrollPosition; // clear out the list of cell view sizes and create a new list _cellViewSizeArray.Clear(); var offset = _AddCellViewSizes(); // if looping, we need to create three sets of size data if (loop) { // if the cells don't entirely fill up the scroll area, // make some more size entries to fill it up if (offset < ScrollRectSize) { int additionalRounds = Mathf.CeilToInt(ScrollRectSize / offset); _DuplicateCellViewSizes(additionalRounds, _cellViewSizeArray.Count); } // set up the loop indices _loopFirstCellIndex = _cellViewSizeArray.Count; _loopLastCellIndex = _loopFirstCellIndex + _cellViewSizeArray.Count - 1; // create two more copies of the cell sizes _DuplicateCellViewSizes(2, _cellViewSizeArray.Count); } // calculate the offsets of each cell view _CalculateCellViewOffsets(); // set the size of the active cell view container based on the number of cell views there are and each of their sizes if (scrollDirection == ScrollDirectionEnum.Vertical) _container.sizeDelta = new Vector2(_container.sizeDelta.x, _cellViewOffsetArray.Last() + padding.top + padding.bottom); else _container.sizeDelta = new Vector2(_cellViewOffsetArray.Last() + padding.left + padding.right, _container.sizeDelta.y); // if looping, set up the loop positions and triggers if (loop) { _loopFirstScrollPosition = GetScrollPositionForCellViewIndex(_loopFirstCellIndex, CellViewPositionEnum.Before) + (spacing * 0.5f); _loopLastScrollPosition = GetScrollPositionForCellViewIndex(_loopLastCellIndex, CellViewPositionEnum.After) - ScrollRectSize + (spacing * 0.5f); _loopFirstJumpTrigger = _loopFirstScrollPosition - ScrollRectSize; _loopLastJumpTrigger = _loopLastScrollPosition + ScrollRectSize; } // create the visibile cells _ResetVisibleCellViews(); // if we need to maintain our original position if (keepPosition) { ScrollPosition = originalScrollPosition; } else { if (loop) { ScrollPosition = _loopFirstScrollPosition; } else { ScrollPosition = 0; } } // set up the visibility of the scrollbar ScrollbarVisibility = scrollbarVisibility; } /// /// Creates a list of cell view sizes for faster access /// /// private float _AddCellViewSizes() { var offset = 0f; // add a size for each row in our data based on how many the delegate tells us to create for (var i = 0; i < NumberOfCells; i++) { // add the size of this cell based on what the delegate tells us to use. Also add spacing if this cell isn't the first one _cellViewSizeArray.Add(_delegate.GetCellViewSize(this, i) + (i == 0 ? 0 : _layoutGroup.spacing)); offset += _cellViewSizeArray[_cellViewSizeArray.Count - 1]; } return offset; } /// /// Create a copy of the cell view sizes. This is only used in looping /// /// How many times the copy should be made /// How many cells to copy private void _DuplicateCellViewSizes(int numberOfTimes, int cellCount) { for (var i = 0; i < numberOfTimes; i++) { for (var j = 0; j < cellCount; j++) { _cellViewSizeArray.Add(_cellViewSizeArray[j] + (j == 0 ? _layoutGroup.spacing : 0)); } } } /// /// Calculates the offset of each cell, accumulating the values from previous cells /// private void _CalculateCellViewOffsets() { _cellViewOffsetArray.Clear(); var offset = 0f; for (var i = 0; i < _cellViewSizeArray.Count; i++) { offset += _cellViewSizeArray[i]; _cellViewOffsetArray.Add(offset); } } /// /// Get a recycled cell with a given identifier if available /// /// The prefab to check for /// private EnhancedScrollerCellView _GetRecycledCellView(EnhancedScrollerCellView cellPrefab) { for (var i = 0; i < _recycledCellViews.Count; i++) { if (_recycledCellViews[i].cellIdentifier == cellPrefab.cellIdentifier) { // the cell view was found, so we use this recycled one. // we also remove it from the recycled list var cellView = _recycledCellViews.RemoveAt(i); return cellView; } } return null; } /// /// This sets up the visible cells, adding and recycling as necessary /// private void _ResetVisibleCellViews() { int startIndex; int endIndex; // calculate the range of the visible cells _CalculateCurrentActiveCellRange(out startIndex, out endIndex); // go through each previous active cell and recycle it if it no longer falls in the range var i = 0; SmallList remainingCellIndices = new SmallList(); while (i < _activeCellViews.Count) { if (_activeCellViews[i].cellIndex < startIndex || _activeCellViews[i].cellIndex > endIndex) { _RecycleCell(_activeCellViews[i]); } else { // this cell index falls in the new range, so we add its // index to the reusable list remainingCellIndices.Add(_activeCellViews[i].cellIndex); i++; } } if (remainingCellIndices.Count == 0) { // there were no previous active cells remaining, // this list is either brand new, or we jumped to // an entirely different part of the list. // just add all the new cell views for (i = startIndex; i <= endIndex; i++) { _AddCellView(i, ListPositionEnum.Last); } } else { // we are able to reuse some of the previous // cell views // first add the views that come before the // previous list, going backward so that the // new views get added to the front for (i = endIndex; i >= startIndex; i--) { if (i < remainingCellIndices.First()) { _AddCellView(i, ListPositionEnum.First); } } // next add teh views that come after the // previous list, going forward and adding // at the end of the list for (i = startIndex; i <= endIndex; i++) { if (i > remainingCellIndices.Last()) { _AddCellView(i, ListPositionEnum.Last); } } } // update the start and end indices _activeCellViewsStartIndex = startIndex; _activeCellViewsEndIndex = endIndex; // adjust the padding elements to offset the cell views correctly _SetPadders(); } /// /// Recycles all the active cells /// private void _RecycleAllCells() { while (_activeCellViews.Count > 0) _RecycleCell(_activeCellViews[0]); _activeCellViewsStartIndex = 0; _activeCellViewsEndIndex = 0; } /// /// Recycles one cell view /// /// private void _RecycleCell(EnhancedScrollerCellView cellView) { // remove the cell view from the active list _activeCellViews.Remove(cellView); // add the cell view to the recycled list _recycledCellViews.Add(cellView); // move the GameObject to the recycled container cellView.transform.SetParent(_recycledCellViewContainer); // reset the cellView's properties cellView.dataIndex = 0; cellView.cellIndex = 0; cellView.active = false; if (cellViewVisibilityChanged != null) cellViewVisibilityChanged(cellView); } /// /// Creates a cell view, or recycles if it can /// /// The index of the cell view /// Whether to add the cell to the beginning or the end private void _AddCellView(int cellIndex, ListPositionEnum listPosition) { if (NumberOfCells == 0) return; // get the dataIndex. Modulus is used in case of looping so that the first set of cells are ignored var dataIndex = cellIndex % NumberOfCells; // request a cell view from the delegate var cellView = _delegate.GetCellView(this, dataIndex, cellIndex); // set the cell's properties cellView.cellIndex = cellIndex; cellView.dataIndex = dataIndex; cellView.active = true; // add the cell view to the active container cellView.transform.SetParent(_container, false); cellView.transform.localScale = Vector3.one; // add a layout element to the cellView LayoutElement layoutElement = cellView.GetComponent(); if (layoutElement == null) layoutElement = cellView.gameObject.AddComponent(); // set the size of the layout element if (scrollDirection == ScrollDirectionEnum.Vertical) layoutElement.minHeight = _cellViewSizeArray[cellIndex] - (cellIndex > 0 ? _layoutGroup.spacing : 0); else layoutElement.minWidth = _cellViewSizeArray[cellIndex] - (cellIndex > 0 ? _layoutGroup.spacing : 0); // add the cell to the active list if (listPosition == ListPositionEnum.First) _activeCellViews.AddStart(cellView); else _activeCellViews.Add(cellView); // set the hierarchy position of the cell view in the container if (listPosition == ListPositionEnum.Last) cellView.transform.SetSiblingIndex(_container.childCount - 2); else if (listPosition == ListPositionEnum.First) cellView.transform.SetSiblingIndex(1); // call the visibility change delegate if available if (cellViewVisibilityChanged != null) cellViewVisibilityChanged(cellView); } /// /// This function adjusts the two padders that control the first cell view's /// offset and the overall size of each cell. /// private void _SetPadders() { if (NumberOfCells == 0) return; // calculate the size of each padder var firstSize = _cellViewOffsetArray[_activeCellViewsStartIndex] - _cellViewSizeArray[_activeCellViewsStartIndex]; var lastSize = _cellViewOffsetArray.Last() - _cellViewOffsetArray[_activeCellViewsEndIndex]; if (scrollDirection == ScrollDirectionEnum.Vertical) { // set the first padder and toggle its visibility _firstPadder.minHeight = firstSize; _firstPadder.gameObject.SetActive(_firstPadder.minHeight > 0); // set the last padder and toggle its visibility _lastPadder.minHeight = lastSize; _lastPadder.gameObject.SetActive(_lastPadder.minHeight > 0); } else { // set the first padder and toggle its visibility _firstPadder.minWidth = firstSize; _firstPadder.gameObject.SetActive(_firstPadder.minWidth > 0); // set the last padder and toggle its visibility _lastPadder.minWidth = lastSize; _lastPadder.gameObject.SetActive(_lastPadder.minWidth > 0); } } /// /// This function is called if the scroller is scrolled, updating the active list of cells /// private void _RefreshActive() { _refreshActive = false; int startIndex; int endIndex; var velocity = Vector2.zero; // if looping, check to see if we scrolled past a trigger if (loop) { if (_scrollPosition < _loopFirstJumpTrigger) { velocity = _scrollRect.velocity; ScrollPosition = _loopLastScrollPosition - (_loopFirstJumpTrigger - _scrollPosition); _scrollRect.velocity = velocity; } else if (_scrollPosition > _loopLastJumpTrigger) { velocity = _scrollRect.velocity; ScrollPosition = _loopFirstScrollPosition + (_scrollPosition - _loopLastJumpTrigger); _scrollRect.velocity = velocity; } } // get the range of visibile cells _CalculateCurrentActiveCellRange(out startIndex, out endIndex); // if the index hasn't changed, ignore and return if (startIndex == _activeCellViewsStartIndex && endIndex == _activeCellViewsEndIndex) return; // recreate the visibile cells _ResetVisibleCellViews(); } /// /// Determines which cells can be seen /// /// The index of the first cell visible /// The index of the last cell visible private void _CalculateCurrentActiveCellRange(out int startIndex, out int endIndex) { startIndex = 0; endIndex = 0; // get the positions of the scroller var startPosition = _scrollPosition; var endPosition = _scrollPosition + (scrollDirection == ScrollDirectionEnum.Vertical ? _scrollRectTransform.rect.height : _scrollRectTransform.rect.width); // calculate each index based on the positions startIndex = GetCellViewIndexAtPosition(startPosition); endIndex = GetCellViewIndexAtPosition(endPosition); } /// /// Gets the index of a cell at a given position based on a subset range. /// This function uses a recursive binary sort to find the index faster. /// /// The pixel offset from the start of the scroller /// The first index of the range /// The last index of the rnage /// private int _GetCellIndexAtPosition(float position, int startIndex, int endIndex) { // if the range is invalid, then we found our index, return the start index if (startIndex >= endIndex) return startIndex; // determine the middle point of our binary search var middleIndex = (startIndex + endIndex) / 2; // if the middle index is greater than the position, then search the last // half of the binary tree, else search the first half if ((_cellViewOffsetArray[middleIndex] + (scrollDirection == ScrollDirectionEnum.Vertical ? padding.top : padding.left)) >= position) return _GetCellIndexAtPosition(position, startIndex, middleIndex); else return _GetCellIndexAtPosition(position, middleIndex + 1, endIndex); } /// /// Caches and initializes the scroller /// void Awake() { GameObject go; // cache some components _scrollRect = this.GetComponent(); _scrollRectTransform = _scrollRect.GetComponent(); // destroy any content objects if they exist. Likely there will be // one at design time because Unity gives errors if it can't find one. if (_scrollRect.content != null) { DestroyImmediate(_scrollRect.content.gameObject); } // Create a new active cell view container with a layout group go = new GameObject("Container", typeof(RectTransform)); go.transform.SetParent(_scrollRectTransform); if (scrollDirection == ScrollDirectionEnum.Vertical) go.AddComponent(); else go.AddComponent(); _container = go.GetComponent(); _container.localEulerAngles = Vector3.zero; Vector3 pos3D = _container.anchoredPosition3D; pos3D.z = 0; _container.anchoredPosition3D = pos3D; // set the containers anchor and pivot if (scrollDirection == ScrollDirectionEnum.Vertical) { _container.anchorMin = new Vector2(0, 1); _container.anchorMax = Vector2.one; _container.pivot = new Vector2(0.5f, 1f); } else { _container.anchorMin = Vector2.zero; _container.anchorMax = new Vector2(0, 1f); _container.pivot = new Vector2(0, 0.5f); } _container.offsetMax = Vector2.zero; _container.offsetMin = Vector2.zero; _container.localScale = Vector3.one; _scrollRect.content = _container; // cache the scrollbar if it exists if (scrollDirection == ScrollDirectionEnum.Vertical) { _scrollbar = _scrollRect.verticalScrollbar; } else { _scrollbar = _scrollRect.horizontalScrollbar; } // cache the layout group and set up its spacing and padding _layoutGroup = _container.GetComponent(); _layoutGroup.spacing = spacing; _layoutGroup.padding = padding; _layoutGroup.childAlignment = TextAnchor.UpperLeft; _layoutGroup.childForceExpandHeight = true; _layoutGroup.childForceExpandWidth = true; // force the scroller to scroll in the direction we want _scrollRect.horizontal = scrollDirection == ScrollDirectionEnum.Horizontal; _scrollRect.vertical = scrollDirection == ScrollDirectionEnum.Vertical; // create the padder objects go = new GameObject("First Padder", typeof(RectTransform), typeof(LayoutElement)); go.transform.SetParent(_container, false); _firstPadder = go.GetComponent(); go = new GameObject("Last Padder", typeof(RectTransform), typeof(LayoutElement)); go.transform.SetParent(_container, false); _lastPadder = go.GetComponent(); // create the recycled cell view container go = new GameObject("Recycled Cells", typeof(RectTransform)); go.transform.SetParent(_scrollRect.transform, false); _recycledCellViewContainer = go.GetComponent(); _recycledCellViewContainer.gameObject.SetActive(false); // set up the last values for updates _lastScrollRectSize = ScrollRectSize; _lastLoop = loop; _lastScrollbarVisibility = scrollbarVisibility; } void Update() { if (_reloadData) { // if the reload flag is true, then reload the data ReloadData(); } // if the scroll rect size has changed and looping is on, // or the loop setting has changed, then we need to resize if ( (loop && _lastScrollRectSize != ScrollRectSize) || (loop != _lastLoop) ) { _Resize(true); _lastScrollRectSize = ScrollRectSize; _lastLoop = loop; } // update the scroll bar visibility if it has changed if (_lastScrollbarVisibility != scrollbarVisibility) { ScrollbarVisibility = scrollbarVisibility; _lastScrollbarVisibility = scrollbarVisibility; } // determine if the scroller has started or stopped scrolling // and call the delegate if so. if (LinearVelocity != 0 && !IsScrolling) { IsScrolling = true; if (scrollerScrollingChanged != null) scrollerScrollingChanged(this, true); } else if (LinearVelocity == 0 && IsScrolling) { IsScrolling = false; if (scrollerScrollingChanged != null) scrollerScrollingChanged(this, false); } } void LateUpdate() { if (_refreshActive) { // if the refresh toggle is on, then // refresh the list _RefreshActive(); } } void OnEnable() { // when the scroller is enabled, add a listener to the onValueChanged handler _scrollRect.onValueChanged.AddListener(_ScrollRect_OnValueChanged); } void OnDisable() { // when the scroller is disabled, remove the listener _scrollRect.onValueChanged.RemoveListener(_ScrollRect_OnValueChanged); } /// /// Handler for when the scroller changes value /// /// The scroll rect's value private void _ScrollRect_OnValueChanged(Vector2 val) { // set the internal scroll position if (scrollDirection == ScrollDirectionEnum.Vertical) _scrollPosition = (1f - val.y) * _ScrollSize; else _scrollPosition = val.x * _ScrollSize; _refreshActive = true; // call the handler if it exists if (scrollerScrolled != null) scrollerScrolled(this, val, _scrollPosition); // if the snapping is turned on, handle it if (snapping && !_snapJumping) { // if the speed has dropped below the threshhold velocity if (Mathf.Abs(LinearVelocity) <= snapVelocityThreshold) { // Call the snap function Snap(); } } _RefreshActive(); } /// /// This is fired by the tweener when the snap tween is completed /// private void SnapJumpComplete() { // reset the snap jump to false and restore the inertia state _snapJumping = false; _scrollRect.inertia = _snapInertia; // fire the scroller snapped delegate if (scrollerSnapped != null) scrollerSnapped(this, _snapCellViewIndex, _snapDataIndex); } #endregion #region Tweening /// /// The easing type /// public enum TweenType { immediate, linear, spring, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInQuint, easeOutQuint, easeInOutQuint, easeInSine, easeOutSine, easeInOutSine, easeInExpo, easeOutExpo, easeInOutExpo, easeInCirc, easeOutCirc, easeInOutCirc, easeInBounce, easeOutBounce, easeInOutBounce, easeInBack, easeOutBack, easeInOutBack, easeInElastic, easeOutElastic, easeInOutElastic } private float _tweenTimeLeft; /// /// Moves the scroll position over time between two points given an easing function. When the /// tween is complete it will fire the jumpComplete delegate. /// /// The type of easing to use /// The amount of time to interpolate /// The starting scroll position /// The ending scroll position /// The action to fire when the tween is complete /// IEnumerator TweenPosition(TweenType tweenType, float time, float start, float end, Action tweenComplete) { if (tweenType == TweenType.immediate || time == 0) { // if the easing is immediate or the time is zero, just jump to the end position ScrollPosition = end; } else { // zero out the velocity _scrollRect.velocity = Vector2.zero; // fire the delegate for the tween start IsTweening = true; if (scrollerTweeningChanged != null) scrollerTweeningChanged(this, true); _tweenTimeLeft = 0; var newPosition = 0f; // while the tween has time left, use an easing function while (_tweenTimeLeft < time) { switch (tweenType) { case TweenType.linear: newPosition = linear(start, end, (_tweenTimeLeft / time)); break; case TweenType.spring: newPosition = spring(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInQuad: newPosition = easeInQuad(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutQuad: newPosition = easeOutQuad(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutQuad: newPosition = easeInOutQuad(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInCubic: newPosition = easeInCubic(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutCubic: newPosition = easeOutCubic(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutCubic: newPosition = easeInOutCubic(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInQuart: newPosition = easeInQuart(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutQuart: newPosition = easeOutQuart(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutQuart: newPosition = easeInOutQuart(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInQuint: newPosition = easeInQuint(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutQuint: newPosition = easeOutQuint(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutQuint: newPosition = easeInOutQuint(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInSine: newPosition = easeInSine(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutSine: newPosition = easeOutSine(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutSine: newPosition = easeInOutSine(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInExpo: newPosition = easeInExpo(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutExpo: newPosition = easeOutExpo(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutExpo: newPosition = easeInOutExpo(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInCirc: newPosition = easeInCirc(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutCirc: newPosition = easeOutCirc(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutCirc: newPosition = easeInOutCirc(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInBounce: newPosition = easeInBounce(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutBounce: newPosition = easeOutBounce(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutBounce: newPosition = easeInOutBounce(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInBack: newPosition = easeInBack(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutBack: newPosition = easeOutBack(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutBack: newPosition = easeInOutBack(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInElastic: newPosition = easeInElastic(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeOutElastic: newPosition = easeOutElastic(start, end, (_tweenTimeLeft / time)); break; case TweenType.easeInOutElastic: newPosition = easeInOutElastic(start, end, (_tweenTimeLeft / time)); break; } if (loop) { // if we are looping, we need to make sure the new position isn't past the jump trigger. // if it is we need to reset back to the jump position on the other side of the area. if (end > start && newPosition > _loopLastJumpTrigger) { //Debug.Log("name: " + name + " went past the last jump trigger, looping back around"); newPosition = _loopFirstScrollPosition + (newPosition - _loopLastJumpTrigger); } else if (start > end && newPosition < _loopFirstJumpTrigger) { //Debug.Log("name: " + name + " went past the first jump trigger, looping back around"); newPosition = _loopLastScrollPosition - (_loopFirstJumpTrigger - newPosition); } } // set the scroll position to the tweened position ScrollPosition = newPosition; // increase the time elapsed _tweenTimeLeft += Time.unscaledDeltaTime; yield return null; } // the time has expired, so we make sure the final scroll position // is the actual end position. ScrollPosition = end; } // the tween jump is complete, so we fire the delegate if (tweenComplete != null) tweenComplete(); // fire the delegate for the tween ending IsTweening = false; if (scrollerTweeningChanged != null) scrollerTweeningChanged(this, false); } private float linear(float start, float end, float val) { return Mathf.Lerp(start, end, val); } private static float spring(float start, float end, float val) { val = Mathf.Clamp01(val); val = (Mathf.Sin(val * Mathf.PI * (0.2f + 2.5f * val * val * val)) * Mathf.Pow(1f - val, 2.2f) + val) * (1f + (1.2f * (1f - val))); return start + (end - start) * val; } private static float easeInQuad(float start, float end, float val) { end -= start; return end * val * val + start; } private static float easeOutQuad(float start, float end, float val) { end -= start; return -end * val * (val - 2) + start; } private static float easeInOutQuad(float start, float end, float val) { val /= .5f; end -= start; if (val < 1) return end / 2 * val * val + start; val--; return -end / 2 * (val * (val - 2) - 1) + start; } private static float easeInCubic(float start, float end, float val) { end -= start; return end * val * val * val + start; } private static float easeOutCubic(float start, float end, float val) { val--; end -= start; return end * (val * val * val + 1) + start; } private static float easeInOutCubic(float start, float end, float val) { val /= .5f; end -= start; if (val < 1) return end / 2 * val * val * val + start; val -= 2; return end / 2 * (val * val * val + 2) + start; } private static float easeInQuart(float start, float end, float val) { end -= start; return end * val * val * val * val + start; } private static float easeOutQuart(float start, float end, float val) { val--; end -= start; return -end * (val * val * val * val - 1) + start; } private static float easeInOutQuart(float start, float end, float val) { val /= .5f; end -= start; if (val < 1) return end / 2 * val * val * val * val + start; val -= 2; return -end / 2 * (val * val * val * val - 2) + start; } private static float easeInQuint(float start, float end, float val) { end -= start; return end * val * val * val * val * val + start; } private static float easeOutQuint(float start, float end, float val) { val--; end -= start; return end * (val * val * val * val * val + 1) + start; } private static float easeInOutQuint(float start, float end, float val) { val /= .5f; end -= start; if (val < 1) return end / 2 * val * val * val * val * val + start; val -= 2; return end / 2 * (val * val * val * val * val + 2) + start; } private static float easeInSine(float start, float end, float val) { end -= start; return -end * Mathf.Cos(val / 1 * (Mathf.PI / 2)) + end + start; } private static float easeOutSine(float start, float end, float val) { end -= start; return end * Mathf.Sin(val / 1 * (Mathf.PI / 2)) + start; } private static float easeInOutSine(float start, float end, float val) { end -= start; return -end / 2 * (Mathf.Cos(Mathf.PI * val / 1) - 1) + start; } private static float easeInExpo(float start, float end, float val) { end -= start; return end * Mathf.Pow(2, 10 * (val / 1 - 1)) + start; } private static float easeOutExpo(float start, float end, float val) { end -= start; return end * (-Mathf.Pow(2, -10 * val / 1) + 1) + start; } private static float easeInOutExpo(float start, float end, float val) { val /= .5f; end -= start; if (val < 1) return end / 2 * Mathf.Pow(2, 10 * (val - 1)) + start; val--; return end / 2 * (-Mathf.Pow(2, -10 * val) + 2) + start; } private static float easeInCirc(float start, float end, float val) { end -= start; return -end * (Mathf.Sqrt(1 - val * val) - 1) + start; } private static float easeOutCirc(float start, float end, float val) { val--; end -= start; return end * Mathf.Sqrt(1 - val * val) + start; } private static float easeInOutCirc(float start, float end, float val) { val /= .5f; end -= start; if (val < 1) return -end / 2 * (Mathf.Sqrt(1 - val * val) - 1) + start; val -= 2; return end / 2 * (Mathf.Sqrt(1 - val * val) + 1) + start; } private static float easeInBounce(float start, float end, float val) { end -= start; float d = 1f; return end - easeOutBounce(0, end, d - val) + start; } private static float easeOutBounce(float start, float end, float val) { val /= 1f; end -= start; if (val < (1 / 2.75f)) { return end * (7.5625f * val * val) + start; } else if (val < (2 / 2.75f)) { val -= (1.5f / 2.75f); return end * (7.5625f * (val) * val + .75f) + start; } else if (val < (2.5 / 2.75)) { val -= (2.25f / 2.75f); return end * (7.5625f * (val) * val + .9375f) + start; } else { val -= (2.625f / 2.75f); return end * (7.5625f * (val) * val + .984375f) + start; } } private static float easeInOutBounce(float start, float end, float val) { end -= start; float d = 1f; if (val < d / 2) return easeInBounce(0, end, val * 2) * 0.5f + start; else return easeOutBounce(0, end, val * 2 - d) * 0.5f + end * 0.5f + start; } private static float easeInBack(float start, float end, float val) { end -= start; val /= 1; float s = 1.70158f; return end * (val) * val * ((s + 1) * val - s) + start; } private static float easeOutBack(float start, float end, float val) { float s = 1.70158f; end -= start; val = (val / 1) - 1; return end * ((val) * val * ((s + 1) * val + s) + 1) + start; } private static float easeInOutBack(float start, float end, float val) { float s = 1.70158f; end -= start; val /= .5f; if ((val) < 1) { s *= (1.525f); return end / 2 * (val * val * (((s) + 1) * val - s)) + start; } val -= 2; s *= (1.525f); return end / 2 * ((val) * val * (((s) + 1) * val + s) + 2) + start; } private static float easeInElastic(float start, float end, float val) { end -= start; float d = 1f; float p = d * .3f; float s = 0; float a = 0; if (val == 0) return start; val = val / d; if (val == 1) return start + end; if (a == 0f || a < Mathf.Abs(end)) { a = end; s = p / 4; } else { s = p / (2 * Mathf.PI) * Mathf.Asin(end / a); } val = val - 1; return -(a * Mathf.Pow(2, 10 * val) * Mathf.Sin((val * d - s) * (2 * Mathf.PI) / p)) + start; } private static float easeOutElastic(float start, float end, float val) { end -= start; float d = 1f; float p = d * .3f; float s = 0; float a = 0; if (val == 0) return start; val = val / d; if (val == 1) return start + end; if (a == 0f || a < Mathf.Abs(end)) { a = end; s = p / 4; } else { s = p / (2 * Mathf.PI) * Mathf.Asin(end / a); } return (a * Mathf.Pow(2, -10 * val) * Mathf.Sin((val * d - s) * (2 * Mathf.PI) / p) + end + start); } private static float easeInOutElastic(float start, float end, float val) { end -= start; float d = 1f; float p = d * .3f; float s = 0; float a = 0; if (val == 0) return start; val = val / (d / 2); if (val == 2) return start + end; if (a == 0f || a < Mathf.Abs(end)) { a = end; s = p / 4; } else { s = p / (2 * Mathf.PI) * Mathf.Asin(end / a); } if (val < 1) { val = val - 1; return -0.5f * (a * Mathf.Pow(2, 10 * val) * Mathf.Sin((val * d - s) * (2 * Mathf.PI) / p)) + start; } val = val - 1; return a * Mathf.Pow(2, -10 * val) * Mathf.Sin((val * d - s) * (2 * Mathf.PI) / p) * 0.5f + end + start; } #endregion } }